﻿package com.musicrun.midi
{
	import com.musicrun.utilities.MyEventDispatcher;
	import com.newgonzo.midi.MIDIClock;
	import com.newgonzo.midi.MIDIDecoder;
	import com.newgonzo.midi.events.*;
	import com.newgonzo.midi.file.*;
	import com.newgonzo.midi.file.MIDITrack;
	import com.newgonzo.midi.file.messages.SetTempoMessage;
	import com.newgonzo.midi.file.messages.TimeSignatureMessage;
	import com.newgonzo.midi.io.*;
	import com.newgonzo.midi.messages.*;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.net.FileFilter;
	import flash.net.FileReference;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.trace.Trace;
	import flash.utils.ByteArray;
	
	/**
	 * Represents a MIDI song. Takes in the URL to the song, and reads
	 * it into a dat astructure that can be easily processed in a time-based
	 * manner.
	 **/
	public class Song
	{
		private var loader:URLLoader = new URLLoader();
		
		var tracks:Vector.<Track>;

		/**
		 * Ticks are the nunit timestamps of the midi notes come in. This number
		 * will likely be something like 24.
		 **/
		private var ticksForBeat:int;
		private var pulsesPerQuarter:int;
		private var numerator:int;
		private var denominator:int;
		/**
		 * This is the value that I get straight form the midi.
		 */
		private var microsecondsPerQuarter:int; 
		
		/**
		 * This is the value I actually use for my timer.
		 * The MIDI file gives us the tempo in microseconds to beat, 
		 * but I am going to round to the nearest millisecond per tick,
		 * losing some tempo accuracy, but making things easier to calculate
		 * later on.
		 **/
		private var millisecondsPerTick:Number;
		
		public function Song(songURL:String)
		{
			tracks = new Vector.<Track>();
			
			try
			{
				this.loader.dataFormat = URLLoaderDataFormat.BINARY;
				this.loader.load(new URLRequest(songURL));
				this.loader.addEventListener(Event.COMPLETE, handleLoadComplete);
			}
			catch (e:Error)
			{
				trace("Failed to load file: " + songURL);
			}
		}
		
		//-----------------------------
		//  Handlers
		//-----------------------------
		
		private function handleLoadComplete(event:Event):void
		{
			this.loader.removeEventListener(Event.COMPLETE, handleLoadComplete);
			
			var midiData:ByteArray = loader.data as ByteArray;
			
			var decoder:MIDIDecoder = new MIDIDecoder();
			var file:MIDIFile = decoder.decodeFile(midiData);
			
			//a MIDI file has an array of tracks that each contain an array of track events
			var midiTrack:MIDITrack;
			var midiEvent:MIDITrackEvent;
			
			trace("Number of tracks: " + file.numTracks);
			
			//For the game to work, I need to recieve both a tempo and a time signature.
			//Set flags to make sure we got both of thesel
			var gotTempo:Boolean = false;
			var gotTimeSignature:Boolean = false;
			
			var track:Track;
			for each (midiTrack in file.tracks)
			{
				//Create a new one of our track objects.
				track = new Track();
				tracks.push(track);
				
				for each (midiEvent in midiTrack.events)
				{
					if (midiEvent.message is VoiceMessage)
					{
						handleVoiceMessage(track, midiEvent.time, midiEvent.message as VoiceMessage);
					}
					else if (midiEvent.message is ChannelMessage)
					{
						handleChannelMessage(track, midiEvent.message as ChannelMessage);
					}
					else if (midiEvent.message is TimeSignatureMessage)
					{
						handleTimeSignatureMessage(midiEvent.message as TimeSignatureMessage);
						gotTempo = true;
					}
					else if (midiEvent.message is SetTempoMessage)
					{
						handleSetTempo(midiEvent.message as SetTempoMessage);
						gotTimeSignature = true;
					}
					//trace("event.time: " + midiEvent.time);
					//trace("event.message: " + midiEvent.message); // [Message(status=...]
				}
				trace(midiTrack);
			}
			
			//Now as a test, iterate over all of the notes we've recieved
			for each (track in tracks) {
				trace(track);
			}
			
			if (!gotTempo || !gotTimeSignature)
			{
				trace("The input midi file is missing a tempo or a time signature.");
				MyEventDispatcher.getInstance().dispatchEvent(new SongParsedEvent(false));
			}
			else
			{
				calculateTimerTempo();
				
				trace("MIDI Parsing complete.");
				MyEventDispatcher.getInstance().dispatchEvent(new SongParsedEvent(true));
			}
		}
		
		private function handleSetTempo(message:SetTempoMessage):void
		{
			this.microsecondsPerQuarter = message.microsecondsPerQuarter;
		}
		
		private function handleTimeSignatureMessage(message:TimeSignatureMessage):void
		{
			this.ticksForBeat = message.clocksPerBeat;
			this.pulsesPerQuarter = message.thirtySecondthsPerQuarter;
			this.numerator = message.numerator;
			this.denominator = message.denominator;
		}
		
		private function handleChannelMessage(track:Track, message:ChannelMessage):void
		{
			track.setChannelNumber(message.channel);
		}
		
		private function handleVoiceMessage(track:Track, time:int, message:VoiceMessage):void
		{
			var noteNumber:int = message.pitch;
			var velocity:int = message.velocity;
			
			var note:Note;
			if (velocity > 0)
			{
				//this is a note on, add a new note to the track
				note = new Note(time, noteNumber, velocity);
				track.addNote(note, time);
			}
			else
			{
				//this is a note off
				
				//find the most recently occured note that is of this pitch, and assign its note off time
				for (var t:int = time; t >= 0; t--)
				{
					note = track.getNote(t);
					if (note != null && note.getNoteNumber() == noteNumber)
					{
						//found the note on that occured before this note off
						note.setTimeOfNoteOff(time);
						break;
					}
				}
			}
		}
		
		//-----------------------------
		//  Getters / Setters
		//-----------------------------
		
		public function getMilliSecsPerTick():Number
		{
			return this.millisecondsPerTick;
		}
		
		public function getTracks():Vector.<Track>
		{
			return this.tracks;
		}
		
		//-----------------------------
		//  Helper Methods
		//-----------------------------
		
		/**
		 * This should only be called once we are certain we have recieved
		 * both the tempomessage and the timesignature message
		 */
		private function calculateTimerTempo()
		{
			// convert from microseconds per quarter to milliseconds per tick
			/*    
			*     micros	quarters	milliscnds	 beat	 milliseconds
			*     ------- * -------- *	---------- * ----- = ------------
			*     quarter	beat    	micros    	 ticks	 tick
			*/
			var quartersPerBeat:Number = 4.0 / this.denominator;
			var microsPerBeat:Number = this.microsecondsPerQuarter * quartersPerBeat;
			var millisPerMicro:Number = 0.001;
			var millisecondsPerBeat:Number = microsPerBeat * millisPerMicro;
			this.millisecondsPerTick = millisecondsPerBeat / this.ticksForBeat;
		}
	}
}